package spaceinvaders;

/*
 * Copyright (c) 2002-2008 LWJGL Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'LWJGL' nor the names of
 *   its contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.util.ArrayList;

import open_graphics_library.OpenGraphicsLibrary;

import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;

/**
 * The main hook of our game. This class with both act as a manager for the
 * display and central mediator for the game logic.
 * 
 * Display management will consist of a loop that cycles round all entities in
 * the game asking them to move and then drawing them in the appropriate place.
 * With the help of an inner class it will also allow the player to control the
 * main ship.
 * 
 * As a mediator it will be informed when entities within our game detect events
 * (e.g. alient killed, played died) and will take appropriate game actions.
 * 
 * <p>
 * NOTE:<br>
 * This game is a LWJGLized implementation of the Space Invaders game by Kevin
 * Glass. The original implementation is renderer agnostic and supports other
 * OpenGL implementations as well as Java2D. This version has been made specific
 * for LWJGL, and has added input control as well as sound (which the original
 * doesn't, at the time of writing). You can find the original article here:<br>
 * <a href="http://www.cokeandcode.com/"
 * target="_blank">http://www.cokeandcode.com</a>
 * </p>
 * 
 * @author Kevin Glass
 * @author Brian Matzon
 */
public class Game {

	// TODO N Probably make this configurable when making this a more generel
	// lib.
	/** Desired frame time */
	private static final int m_nFRAMERATE = 60;
	private static final int m_nPIXEL_HEIGHT = 800;
	private static final int m_nPIXEL_WIDTH = 600;

	/** The loader responsible for converting images into OpenGL textures */
	private TextureLoader m_textureLoader;

	/** The list of all the entities that exist in our game */
	private ArrayList<Entity> m_entities = new ArrayList<Entity>();

	/** The list of entities that need to be removed from the game this loop */
	private ArrayList<Entity> m_removeList = new ArrayList<Entity>();

	/** The entity representing the player */
	private ShipEntity m_ship;

	/** The speed at which the player's ship should move (pixels/sec) */
	private float m_moveSpeed = 300;

	/** The number of aliens left on the screen */
	// private int alienCount;

	/** True if we're holding up game play until a key has been pressed */
	private boolean m_waitingForKeyPress = true;

	/** True if the fire key has been released */
	private boolean m_fireHasBeenReleased;

	/**
	 * True if game logic needs to be applied this loop, normally as a result of
	 * a game event
	 */
	private boolean m_logicRequiredThisLoop;

	/**
	 * The time at which the last rendering looped started from the point of
	 * view of the game logic
	 */
	private long m_lastLoopTime = getTime();

	/** The time since the last record of fps */
	private long m_lastFpsTime;

	/** The recorded fps */
	private int m_fps;

	private static long m_timerTicksPerSecond = Sys.getTimerResolution();

	/** True if the game is currently "running", i.e. the game loop is looping */
	public static boolean m_gameRunning = true;

	/** SoundManager to make sound with */
	// TODO add private SoundManager soundManager;

	/** ID of shot effect */
	// private int SOUND_SHOT;

	/** ID of hit effect */
	// private int SOUND_HIT;

	/** ID of start sound */
	// private int SOUND_START;

	/** ID of win sound */
	// private int SOUND_WIN;

	/** ID of loose sound */
	// private int SOUND_LOOSE;

	/** Mouse movement on x axis */
	private int m_mouseX;
	/** Mouse movement on x axis */
	private int m_mouseY;

	/** Is this an application or applet */
	private static boolean m_isApplication;

	OpenGraphicsLibrary m_openGraphicsLibrary;

	private String m_windowTitle;

	/**
	 * Construct our game and set it running.
	 * 
	 * @param fullscreen
	 * 
	 */
	public Game(boolean fullscreen, String windowTitle, Boolean isApplication) {
		m_openGraphicsLibrary = new OpenGraphicsLibrary(m_nPIXEL_HEIGHT,
				m_nPIXEL_WIDTH, m_nFRAMERATE, fullscreen);
		m_windowTitle = windowTitle;
		m_isApplication = isApplication;
		initialize();
	}

	/**
	 * Get the high resolution time in milliseconds
	 * 
	 * @return The high resolution time in milliseconds
	 */
	public static long getTime() {
		// we get the "timer ticks" from the high resolution timer
		// multiply by 1000 so our end result is in milliseconds
		// then divide by the number of ticks in a second giving
		// us a nice clear time in milliseconds
		return (Sys.getTime() * 1000) / m_timerTicksPerSecond;
	}

	/**
	 * Sleep for a fixed number of milliseconds.
	 * 
	 * @param duration
	 *            The amount of time in milliseconds to sleep for
	 */
	public static void sleep(long duration) {
		try {
			Thread.sleep((duration * m_timerTicksPerSecond) / 1000);
		} catch (InterruptedException inte) {
		}
	}

	/**
	 * Intialise the common elements for the game
	 */
	public void initialize() {
		// initialize the window beforehand
		m_openGraphicsLibrary.setTitle(m_windowTitle);

		m_textureLoader = new TextureLoader();

		// create our sound manager, and initialize it with 7 channels
		// 1 channel for sounds, 6 for effects - this should be enough
		// since we have a most 4 shots on screen at any one time, which
		// leaves
		// us with 2 channels for explosions.
		// soundManager = new SoundManager();
		// soundManager.initialize(8);

		// load our sound data
		// SOUND_SHOT = soundManager.addSound("shot.wav");
		// SOUND_HIT = soundManager.addSound("hit.wav");
		// SOUND_START = soundManager.addSound("start.wav");
		// SOUND_WIN = soundManager.addSound("win.wav");
		// SOUND_LOOSE = soundManager.addSound("loose.wav");

		// get our sprites
		// TODO C Add the Sweeper sprite.

		// setup the initial game state
		startGame();
	}

	/**
	 * Start a fresh game, this should clear out any old data and create a new
	 * set.
	 */
	private void startGame() {
		// clear out any existing entities and intialise a new set
		m_entities.clear();
		initEntities();
	}

	/**
	 * Initialise the starting state of the entities (ship and aliens). Each
	 * entitiy will be added to the overall list of entities in the game.
	 */
	private void initEntities() {
		// create the player ship and place it roughly in the center of the
		// screen
		// TODO V put this in a configuration file somewhere.
		m_ship = new ShipEntity(this, "sweeper.gif", 370, 500);
		m_entities.add(m_ship);

		/*
		 * // create a block of aliens (5 rows, by 12 aliens, spaced evenly)
		 * alienCount = 0; for (int row = 0; row < 5; row++) { for (int x = 0; x
		 * < 12; x++) { Entity alien = new AlienEntity(this, 100 + (x * 50),
		 * (50) + row * 30); entities.add(alien); alienCount++; } }
		 */
	}

	/**
	 * Notification from a game entity that the logic of the game should be run
	 * at the next opportunity (normally as a result of some game event)
	 */
	public void updateLogic() {
		m_logicRequiredThisLoop = true;
	}

	/**
	 * Remove an entity from the game. The entity removed will no longer move or
	 * be drawn.
	 * 
	 * @param entity
	 *            The entity that should be removed
	 */
	public void removeEntity(Entity entity) {
		m_removeList.add(entity);
	}

	/**
	 * Notification that the player has died.
	 */
	public void notifyDeath() {
		if (!m_waitingForKeyPress) {
			// soundManager.playSound(SOUND_LOOSE);
		}
		// message = gotYou;
		m_waitingForKeyPress = true;
	}

	/**
	 * Notification that the player has won since all the aliens are dead.
	 */
	public void notifyWin() {
		// message = youWin;
		m_waitingForKeyPress = true;
		// soundManager.playSound(SOUND_WIN);
	}

	/**
	 * Run the main game loop. This method keeps rendering the scene and
	 * requesting that the callback update its screen.
	 */
	private void gameLoop() {
		while (Game.m_gameRunning) {
			m_openGraphicsLibrary.clearScreen();

			// let subsystem paint
			frameRendering();

			// update window contents
			m_openGraphicsLibrary.updateDisplay();
		}

		// clean up
		// soundManager.destroy();
		m_openGraphicsLibrary.destroyDisplay();
	}

	/**
	 * Notification that a frame is being rendered. Responsible for running game
	 * logic and rendering the scene.
	 */
	// TODO V Possibly move the content around a bit, this is both rendering and
	// game logic in here.
	public void frameRendering() {
		// SystemTimer.sleep(lastLoopTime+10-SystemTimer.getTime());
		m_openGraphicsLibrary.syncDisplay(60);

		// work out how long its been since the last update, this
		// will be used to calculate how far the entities should
		// move this loop
		long currentTime = getTime();
		long delta = currentTime - m_lastLoopTime;
		m_lastLoopTime = currentTime;
		m_lastFpsTime += delta;
		m_fps++;

		// update our FPS counter if a second has passed
		if (m_lastFpsTime >= 1000) {
			m_openGraphicsLibrary.setTitle(m_windowTitle + " (FPS: " + m_fps
					+ ")");
			m_lastFpsTime = 0;
			m_fps = 0;
		}

		// cycle round asking each entity to move itself
		// if (!waitingForKeyPress && !soundManager.isPlayingSound()) {
		if (!m_waitingForKeyPress) {
			for (Entity entity : m_entities) {
				entity.move(delta);
			}
		}

		// cycle round drawing all the entities we have in the game
		for (Entity entity : m_entities) {
			entity.draw();
		}

		// brute force collisions, compare every entity against
		// every other entity. If any of them collide notify
		// both entities that the collision has occured
		for (int p = 0; p < m_entities.size(); p++) {
			for (int s = p + 1; s < m_entities.size(); s++) {
				Entity me = m_entities.get(p);
				Entity him = m_entities.get(s);

				if (me.collidesWith(him)) {
					me.collidedWith(him);
					him.collidedWith(me);
				}
			}
		}

		// remove any entity that has been marked for clear up
		m_entities.removeAll(m_removeList);
		m_removeList.clear();

		// if a game event has indicated that game logic should
		// be resolved, cycle round every entity requesting that
		// their personal logic should be considered.
		if (m_logicRequiredThisLoop) {
			for (Entity entity : m_entities) {
				entity.doLogic();
			}

			m_logicRequiredThisLoop = false;
		}

		// if we're waiting for an "any key" press then draw the
		// current message
		if (m_waitingForKeyPress) {
			// message.draw(325, 250);
		}

		// resolve the movement of the ship. First assume the ship
		// isn't moving. If either cursor key is pressed then
		// update the movement appropraitely
		m_ship.setHorizontalMovement(0);
		m_ship.setVerticalMovement(0);

		// get mouse movement on x axis. We need to get it now, since
		// we can only call getDX ONCE! - secondary calls will yield 0, since
		// there haven't been any movement since last call.
		m_mouseX = Mouse.getDX();
		m_mouseY = Mouse.getDY();

		// we delegate input checking to submethod since we want to check
		// for keyboard, mouse & controller
		// TODO N include a mouse delta class, to make it obvious that the mouse
		// stuff is used in hasInput().
		// TODO V create the Model-view-control thing here.
		// TODO V Then add support for controller.
		boolean leftPressed = hasInput(Keyboard.KEY_LEFT);
		boolean rightPressed = hasInput(Keyboard.KEY_RIGHT);
		boolean upPressed = hasInput(Keyboard.KEY_UP);
		boolean downPressed = hasInput(Keyboard.KEY_DOWN);
		boolean firePressed = hasInput(Keyboard.KEY_SPACE);

		// TODO N Enable reconfiguration of keys.

		// if (!waitingForKeyPress && !soundManager.isPlayingSound()) {
		if (!m_waitingForKeyPress) {
			if ((leftPressed) && (!rightPressed)) {
				m_ship.setHorizontalMovement(-m_moveSpeed);
			} else if ((rightPressed) && (!leftPressed)) {
				m_ship.setHorizontalMovement(m_moveSpeed);
			} else if ((upPressed) && (!downPressed)) {
				System.out.println("DDD Ship should be moving up.");
				m_ship.setVerticalMovement(-m_moveSpeed);
			} else if ((downPressed) && (!upPressed)) {
				System.out.println("DDD Ship should be moving down.");
				m_ship.setVerticalMovement(m_moveSpeed);
			}
		} else {
			if (!firePressed) {
				m_fireHasBeenReleased = true;
			}
			if ((firePressed) && (m_fireHasBeenReleased)) {
				m_waitingForKeyPress = false;
				m_fireHasBeenReleased = false;
				startGame();
			}
		}

		// if escape has been pressed, stop the game
		if ((m_openGraphicsLibrary.isCloseRequested() || Keyboard
				.isKeyDown(Keyboard.KEY_ESCAPE))
				|| Keyboard.isKeyDown(Keyboard.KEY_Q) && m_isApplication) {
			Game.m_gameRunning = false;
		}
	}

	/**
	 * @param direction
	 * @return
	 */
	private boolean hasInput(int direction) {
		boolean bInvestigatedKeyPressed = false;
		if (m_mouseY < 0) {
			System.out.println("DDD m_mouseY < 0.");
		}
		if (m_mouseY > 0) {
			System.out.println("DDD m_mouseY > 0.");
		}
		switch (direction) {
		case Keyboard.KEY_LEFT:
			if (Keyboard.isKeyDown(Keyboard.KEY_LEFT) || m_mouseX < 0) {
				bInvestigatedKeyPressed = true;
				System.out.println("DDD hasInput() KEY_LEFT is pressed.");
			}
			break;

		case Keyboard.KEY_RIGHT:
			if (Keyboard.isKeyDown(Keyboard.KEY_RIGHT) || m_mouseX > 0) {
				bInvestigatedKeyPressed = true;
				System.out.println("DDD hasInput() KEY_RIGHT is pressed.");
			}
			break;

		case Keyboard.KEY_UP:
			if (Keyboard.isKeyDown(Keyboard.KEY_UP) || m_mouseY < 0) {
				bInvestigatedKeyPressed = true;
				System.out.println("DDD hasInput() KEY_UP is pressed.");
			}
			break;

		case Keyboard.KEY_DOWN:
			if (Keyboard.isKeyDown(Keyboard.KEY_DOWN) || m_mouseY > 0) {
				bInvestigatedKeyPressed = true;
				System.out.println("DDD hasInput() KEY_DOWN is pressed.");
			}
			break;

		case Keyboard.KEY_SPACE:
			if (Keyboard.isKeyDown(Keyboard.KEY_SPACE)
					|| Mouse.isButtonDown(0)) {
				bInvestigatedKeyPressed = true;
				System.out.println("DDD hasInput() KEY_SPACE is pressed.");
			}
			break;
		}
		return bInvestigatedKeyPressed;
	}

	/**
	 * The entry point into the game. We'll simply create an instance of class
	 * which will start the display and game loop.
	 * 
	 * @param argv
	 *            The arguments that are passed into our game
	 */
	/*
	 * public static void main(String argv[]) { isApplication = true;
	 * System.out.println("Use -fullscreen for fullscreen mode"); new
	 * Game((argv.length > 0 &&
	 * "-fullscreen".equalsIgnoreCase(argv[0]))).execute(); System.exit(0); }
	 */

	/**
	 *
	 */
	public void execute() {
		gameLoop();
	}

	/**
	 * Create or get a sprite which displays the image that is pointed to in the
	 * classpath by "ref"
	 * 
	 * @param ref
	 *            A reference to the image to load
	 * @return A sprite that can be drawn onto the current graphics context.
	 */
	public Sprite getSprite(String ref) {
		return new Sprite(m_textureLoader, ref);
	}
}
